package com.company.bookstore.common.redis;

import com.company.bookstore.common.jwt.JwtUtil;
import com.company.bookstore.entities.book_module.Book;
import com.company.bookstore.exception.user_admin_module.CustomException;
import com.company.bookstore.exception.user_admin_module.CustomSecondKillEndException;
import com.company.bookstore.schedule.ScheduleController;
import com.company.bookstore.service.book_module.BookService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.redisson.api.*;
import org.redisson.codec.JsonJacksonCodec;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.sql.Time;
import java.text.DateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * RedisUtil
 * redis 工具类
 * @author 吴经纶
 * Redisson文档
 * https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
 * LimitRate
 * https://www.javadoc.io/doc/org.redisson/redisson/3.10.6/org/redisson/api/RRateLimiter.html
 * https://blog.csdn.net/weixin_43931625/article/details/103234206
 * RateType {
 *     OVERALL,             //所有客户端加总限流
 *     PER_CLIENT;          //每个客户端单独计算流量
 * }
 * boolean trySetRate(RateType var1, long var2, long var4, RateIntervalUnit var6);
 *                               //var2为访问数，var4为单位时间，var6为时间单位
 *
 *
 *
 *  * @date 2020-08-27 15:34
 *  * @blame Android Team
 */
@Component
@Slf4j
public class RedisUtil {
    public static final String SECOND_KILL_USER_PREFIX = "kill_user_";
    public static final String SECOND_KILL_PREFIX = "kill_";
    public static final String INTERFACE_LIMIT_PREFIX = "limit_";
    public static final String JWT_PREFIX = "jwt_";
    @Resource
    BookService bookService;
    /**
     *  createOrUpdateJwt 存储token，登录后，redis  有记录则无需重新创建jwt
     *  如果token 过期  则需要刷新 , 若无记录则重建
     *  getJwt  获取redis上的token
     * */
    @Resource
    RedissonClient redissonClient;


    public  void createOrUpdateJwt(String username,String jwt)  {
       RBucket<String> bucket =  redissonClient.getBucket(JWT_PREFIX+username);
//       覆盖操作
       bucket.set(jwt, JwtUtil.Constant.JWT_ALLOW_REFRESH, TimeUnit.MILLISECONDS);
    }
    public  String getJwt(String username)  {
        RBucket<String> bucket =  redissonClient.getBucket(JWT_PREFIX+username);
        return bucket.get();
    }
    public  boolean deleteJwt(String username)  {
        RBucket<String> bucket =  redissonClient.getBucket(JWT_PREFIX+username);
        return bucket.delete();
    }

    public RRateLimiter createOrResetInterfaceLimitMap(String methodName){
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(INTERFACE_LIMIT_PREFIX+methodName);
//        1秒只允许1000个请求（线程）
        rateLimiter.trySetRate(RateType.PER_CLIENT,1000,1, RateIntervalUnit.SECONDS);

        return rateLimiter;
    }
/**
 * Redisson默认的编码器为JsonJacksonCodec，JsonJackson在序列化有双向引用的对象时，会出现无限循环异常。而fastjson在检查出双向引用后会自动用引用符$ref替换，终止循环。
 *
 * 在我的情况中，我序列化了一个service，这个service已被spring托管，而且和另一个service之间也相互注入了，用fastjson能 正常序列化到redis，而JsonJackson则抛出无限循环异常。
 *
 * https://blog.csdn.net/weixin_33756418/article/details/89781241
 *
 * 暂不实现批量发布
 *
 * 缓存热卖的图书
 * */
//    public void putSecondKillBook(Book book,long end,String taskName){
//        RMapCache<String,Book> second_kill_book_map = redissonClient.getMapCache("second_kill_book_map");
//        book.setEndTime(end);
//        second_kill_book_map.fastPut(taskName, book);
//    }
//
//    public Map<String,Book> getSecondKillBookMap(){
//        RMapCache< String ,Book> second_kill_book_map = redissonClient.getMapCache("second_kill_book_map");
//        return second_kill_book_map.readAllMap();
//    }
//    public void deleteSecondKillBook(String taskName){
//        RMapCache<String,Book> second_kill_book_map = redissonClient.getMapCache("second_kill_book_map");
//        second_kill_book_map.fastRemove(taskName);
//    }
    public void secondKillBook(Integer bookId,Integer userId){
        redissonClient.getSet(SECOND_KILL_USER_PREFIX+bookId).add(userId);
    }

    public boolean isSecondKillBook(Integer bookId,Integer userId){
        RSet<Integer> rSet = redissonClient.getSet(SECOND_KILL_USER_PREFIX+bookId);
        return rSet.contains(userId);
    }

    public boolean isSecondKillBookStarted(Integer bookId){
        RAtomicLong rAtomicLong = redissonClient.getAtomicLong(SECOND_KILL_PREFIX+bookId);

        return rAtomicLong.isExists();
    }

    public Integer buySecondKill(Integer bookId) throws CustomException {
        RAtomicLong rAtomicLong = redissonClient.getAtomicLong(SECOND_KILL_PREFIX+bookId);
        if (!rAtomicLong.isExists()){
            throw new CustomException("秒杀没开始",400);
        }
        return (int)rAtomicLong.getAndDecrement();
    }




    /**
     * 有效期为 startTime 和 endTime 控制
     * 频率由corn 表达式控制   ！！-只有status 为1时 并且在有效期内才有能执行 -！！
     * !! - 确保bookId 已存在  - !!!
     * 前端传来corn 表达式， 后端具有corn 合法校验
     * 默认5s从redis中抽取数值，若没有变化，则无需更新数据库，redis上存活时间为  10s？  可能出现宕机秒杀提前结束 但后来又恢复的现象 暂时容忍
     * 延长记录已经完成秒杀用户列表
     * 若已经售空，抛出CustomSecondKillEndException 停止任务执行 status 设置为1（在添加任务需要外加数量不为0校验）
     *
     * 若没有秒杀标志，则取数据读取并写入redis
     * 读取秒杀标志中的值并存入数据库
     * 刷新 缓存时间
     *
     * */

    public void checkSecondKill( JobExecutionContext jobExecutionContext) throws CustomSecondKillEndException {
        log.info("秒杀任务开始");
        Integer bookId = (Integer) jobExecutionContext.getMergedJobDataMap().get("bookId");
        RAtomicLong rAtomicLong = redissonClient.getAtomicLong(SECOND_KILL_PREFIX+bookId);
        RSet<Integer> rSet = redissonClient.getSet(SECOND_KILL_USER_PREFIX+bookId);
        if (!rSet.isExists()){
            rSet.add(0);
        }
        rSet.expire(60,TimeUnit.SECONDS);
        if (!rAtomicLong.isExists()){
          Book book =   bookService.getById(bookId);
          if (book.getRepositoryNum()==0){
              log.info("Method : checkSecondKill ==> insert Book fail");
              CustomSecondKillEndException sc =  new CustomSecondKillEndException();
              sc.setJobExecutionContext(jobExecutionContext);
              throw sc;
          }
          log.info("剩余：{} 本书", book.getRepositoryNum());
          rAtomicLong.set(book.getRepositoryNum());
          rAtomicLong.expire(60, TimeUnit.SECONDS);
          return;
        }
        long number = rAtomicLong.get();
        if (number<=0){
            log.info("Method : checkSecondKill ==> sold out");
            CustomSecondKillEndException sc =  new CustomSecondKillEndException();
            sc.setJobExecutionContext(jobExecutionContext);
            throw sc;
        }

        bookService.updateById(Book.builder().id(bookId).repositoryNum((int)number).build());
        log.info("剩余：{} 本书", number);
        rAtomicLong.expire(30,TimeUnit.SECONDS);
        log.info("Method : checkSecondKill ==> updated" + number);
    }

    /**
     * 存在重启后 无法继续之前的任务的情况
     * 存活时间
     * 假设时间频率为 每周一次
     * 调用时 存活时间一定要比corn 小
     * 使用while 循环
     * 第一次访问没有秒杀标志 则设置图书数，存活时间为5s ，睡眠 2s
     * 下一次访问，假设图书数量没有减为小于0的个数，则将图书数载入到数据库中，刷新存活时间
     * 假设图书数量为0或小于0的个数，则返回0，更新数据库状态，并更新concurrentHashMap中的值，外部函数接受后。停止任务
     *
     * 如果书本没买完，且超过持续时间(小时）,保存到数据库，更新concurrentHashMap的值，打破循环，清楚redis上的值，秒杀结束
     *
     * 内置5秒取一次 每次存活10s
     * second 涵义为秒 持续时间  time 为 存活时间
     * */
    @Deprecated
    public Integer setOrGetNumberOfSecondKill (Integer bookId,Long time,Float second,String taskName)  {
//      持续时间内 循环取出redis 上的 图书数量
        long end  = System.currentTimeMillis()+(long) (second*1000);
        RAtomicLong rAtomicLong = redissonClient.getAtomicLong(SECOND_KILL_PREFIX+bookId);
//        将数据库中的值取出并放入
        Book book = bookService.getBookById(bookId);
        if (book == null){
            log.info("Method : setOrGetNumberOfSecondKill insert Book fail");
            return -1;
        }
//        存储秒杀图书记录
//        putSecondKillBook(book,end,taskName);
        rAtomicLong.set(book.getRepositoryNum());
        log.info("set Book {}  remain :{}",book.getBookName(),book.getRepositoryNum());
//        设置存活时间 睡眠时间为存活时间的2分之一
        rAtomicLong.expire(2*time*1000,TimeUnit.MILLISECONDS);
        long remain = book.getRepositoryNum();
        while (System.currentTimeMillis()<end){
            remain = rAtomicLong.get();
            log.info("while continue remain :{}",remain);
            if (remain <= 0){
                log.info("Method : setOrGetNumberOfSecondKill sold Book out");
                return 0;
            }else {
//                 在有效时间内 更新数据库 并刷新存货时间
                    bookService.updateById(Book.builder().id(bookId).repositoryNum((int)remain).build());
//                    ScheduleController.concurrentHashMap.put(taskName,ScheduleController.EMPTY_SCHEDULE);
                    rAtomicLong.expire(2*time*1000,TimeUnit.MILLISECONDS);

            }
            try {
                Thread.sleep(time*1000);
            }catch (InterruptedException i){
                log.info("Method : setOrGetNumberOfSecondKill is interrupted");
                return -1;
            }
        }
//        书本剩余  秒杀结束  删除秒杀图书记录
//        deleteSecondKillBook(taskName);
        remain = rAtomicLong.getAndDelete();
        if (bookService.updateById(Book.builder().id(bookId).repositoryNum((int)remain).build())){
            ScheduleController.concurrentHashMap.put(taskName,ScheduleController.EMPTY_SCHEDULE);
            return 1;
        }else {
            log.info("Method : setOrGetNumberOfSecondKill update Book fail");
            return  -1;
        }
    }

}
